using System.Linq;

namespace RayDen.Library.Core.Primitives
{
    using System;

    public struct Matrix4x4 : ISerializableValue
    {
        internal float det2x2(float a, float b, float c, float d)
        {
            return a * d - b * c;
        }

        internal float det3x3(float a1, float a2, float a3, float b1, float b2, float b3, float c1, float c2, float c3)
        {
            return a1 * this.det2x2(b2, b3, c2, c3) - b1 * this.det2x2(a2, a3, c2, c3) + c1 * this.det2x2(a2, a3, b2, b3);
        }


        public Matrix4x4 Translate(float dx, float dy, float dz)
        {
            this.Identity();

            this.m30 = dx;
            this.m31 = dy;
            this.m32 = dz;

            return this;
        }


        public Matrix4x4 Transpose()
        {
            return new Matrix4x4(this.m00, this.m10, this.m20, this.m30,
                                 this.m01, this.m11, this.m21, this.m31,
                                 this.m02, this.m12, this.m22, this.m32,
                                 this.m03, this.m13, this.m23, this.m33);
        }

        public Matrix4x4 RotateXaxis(float angle)
        {
            return this.RotateXaxis((float)Math.Sin(angle), (float)Math.Cos(angle));
        }

        public Matrix4x4 RotateYaxis(float angle)
        {
            return this.RotateYaxis((float)Math.Sin(angle), (float)Math.Cos(angle));
        }

        public Matrix4x4 RotateZaxis(float angle)
        {
            return this.RotateZaxis((float)Math.Sin(angle), (float)Math.Cos(angle));
        }

        public Matrix4x4 RotateXaxis(float sina, float cosa)
        {
            this.Identity();

            this.m11 = cosa;
            this.m12 = sina;
            this.m21 = -sina;
            this.m22 = cosa;

            return this;
        }

        public Matrix4x4 RotateYaxis(float sina, float cosa)
        {
            this.Identity();

            this.m00 = cosa;
            this.m02 = -sina;
            this.m20 = sina;
            this.m22 = cosa;

            return this;
        }

        public Matrix4x4 RotateZaxis(float sina, float cosa)
        {
            this.Identity();

            this.m00 = cosa;
            this.m01 = sina;
            this.m10 = -sina;
            this.m11 = cosa;

            return this;
        }

        public Matrix4x4 Scale(float factor)
        {
            this.Identity();
            this.m00 = factor;
            this.m11 = factor;
            this.m22 = factor;

            return this;
        }
        public void SetRow(int r, ref Vector a, float b)
        {
            switch (r)
            {
                case 0:
                    m00 = a.x;
                    m01 = a.y;
                    m02 = a.z;
                    m03 = b;
                    break;
                case 1:
                    m10 = a.x;
                    m11 = a.y;
                    m12 = a.z;
                    m13 = b;
                    break;
                case 2:
                    m20 = a.x;
                    m21 = a.y;
                    m22 = a.z;
                    m23 = b;
                    break;
                case 3:
                    m30 = a.x;
                    m31 = a.y;
                    m32 = a.z;
                    m33 = b;
                    break;
            }
        }
        public Matrix4x4 Scale(float sx, float sy, float sz)
        {
            this.Identity();
            this.m00 = sx;
            this.m11 = sy;
            this.m22 = sz;

            return this;
        }

        public float m00, m10, m20, m30;
        public float m01, m11, m21, m31;
        public float m02, m12, m22, m32;
        public float m03, m13, m23, m33;



        public Matrix4x4(float[,] mt)
        {
            this.m00 = mt[0, 0];
            this.m01 = mt[0, 1];
            this.m02 = mt[0, 2];
            this.m03 = mt[0, 3];
            this.m10 = mt[1, 0];
            this.m11 = mt[1, 1];
            this.m12 = mt[1, 2];
            this.m13 = mt[1, 3];
            this.m20 = mt[2, 0];
            this.m21 = mt[2, 1];
            this.m22 = mt[2, 2];
            this.m23 = mt[2, 3];
            this.m30 = mt[3, 0];
            this.m31 = mt[3, 1];
            this.m32 = mt[3, 2];
            this.m33 = mt[3, 3];
        }
        public Matrix4x4(double[] mt)
        {
            this.m00 = (float)mt[0];
            this.m01 = (float)mt[1];
            this.m02 = (float)mt[2];
            this.m03 = (float)mt[3];
            this.m10 = (float)mt[4];
            this.m11 = (float)mt[5];
            this.m12 = (float)mt[6];
            this.m13 = (float)mt[7];
            this.m20 = (float)mt[8];
            this.m21 = (float)mt[9];
            this.m22 = (float)mt[10];
            this.m23 = (float)mt[11];
            this.m30 = (float)mt[12];
            this.m31 = (float)mt[13];
            this.m32 = (float)mt[14];
            this.m33 = (float)mt[15];
        }
        public Matrix4x4(
            float p00, float p01, float p02, float p03,
            float p10, float p11, float p12, float p13,
            float p20, float p21, float p22, float p23,
            float p30, float p31, float p32, float p33)
        {
            this.m00 = p00;
            this.m01 = p01;
            this.m02 = p02;
            this.m03 = p03;
            this.m10 = p10;
            this.m11 = p11;
            this.m12 = p12;
            this.m13 = p13;
            this.m20 = p20;
            this.m21 = p21;
            this.m22 = p22;
            this.m23 = p23;
            this.m30 = p30;
            this.m31 = p31;
            this.m32 = p32;
            this.m33 = p33;
        }

        public Matrix4x4 Zero()
        {
            this.m00 = this.m01 = this.m02 = this.m03 = 0;
            this.m10 = this.m11 = this.m12 = this.m13 = 0;
            this.m20 = this.m21 = this.m22 = this.m23 = 0;
            this.m30 = this.m31 = this.m32 = this.m33 = 0;

            return this;
        }

        public Matrix4x4 Identity()
        {
            this.m01 = this.m02 = this.m03 = 0;
            this.m10 = this.m12 = this.m13 = 0;
            this.m20 = this.m21 = this.m23 = 0;
            this.m30 = this.m31 = this.m32 = 0;

            this.m00 = this.m11 = this.m22 = this.m33 = 1;

            return this;
        }

        public float this[int index]
        {
            get
            {
                switch (index)
                {
                    case 0:
                        return this.m00;
                    case 1:
                        return this.m01;
                    case 2:
                        return this.m02;
                    case 3:
                        return this.m03;
                    case 4:
                        return this.m10;
                    case 5:
                        return this.m11;
                    case 6:
                        return this.m12;
                    case 7:
                        return this.m13;
                    case 8:
                        return this.m20;
                    case 9:
                        return this.m21;
                    case 10:
                        return this.m22;
                    case 11:
                        return this.m23;
                    case 12:
                        return this.m30;
                    case 13:
                        return this.m31;
                    case 14:
                        return this.m32;
                    case 15:
                        return this.m33;

                    default:
                        throw new ArgumentException("Index");
                }
            }
            set
            {
                switch (index)
                {
                    case 0:
                        this.m00 = value;
                        break;
                    case 1:
                        this.m01 = value;
                        break;
                    case 2:
                        this.m02 = value;
                        break;
                    case 3:
                        this.m03 = value;
                        break;
                    case 4:
                        this.m10 = value;
                        break;
                    case 5:
                        this.m11 = value;
                        break;
                    case 6:
                        this.m12 = value;
                        break;
                    case 7:
                        this.m13 = value;
                        break;
                    case 8:
                        this.m20 = value;
                        break;
                    case 9:
                        this.m21 = value;
                        break;
                    case 10:
                        this.m22 = value;
                        break;
                    case 11:
                        this.m23 = value;
                        break;
                    case 12:
                        this.m30 = value;
                        break;
                    case 13:
                        this.m31 = value;
                        break;
                    case 14:
                        this.m32 = value;
                        break;
                    case 15:
                        this.m33 = value;
                        break;

                    default:
                        throw new ArgumentException("Index");
                }
            }
        }


        public float this[int x, int y]
        {
            get
            {
                var index = x + y * 4;
                return this[index];
            }
            set
            {
                var index = x + y * 4;
                this[index] = value;
            }
        }


    public static Matrix4x4 Perspective(
        float aFov,
        float aNear,
        float aFar)
    {
        // Camera points towards -z.  0 < near < far.
        // Matrix maps z range [-near, -far] to [-1, 1], after homogeneous division.
        float f = 1f / ((float)Math.Tan(aFov * MathLab.M_PI / 360.0f));
        float d = 1f / (aNear - aFar);

        Matrix4x4 r;
        r.m00 = f;    r.m01 = 0.0f; r.m02 = 0.0f;               r.m03 = 0.0f;
        r.m10 = 0.0f; r.m11 = -f;   r.m12 = 0.0f;               r.m13 = 0.0f;
        r.m20 = 0.0f; r.m21 = 0.0f; r.m22 = (aNear + aFar) * d; r.m23 = 2.0f * aNear * aFar * d;
        r.m30 = 0.0f; r.m31 = 0.0f; r.m32 = -1.0f;              r.m33 = 0.0f;

        return r;
    }

        public static Matrix4x4 Inverse(ref Matrix4x4 res)
        {
            float a1, a2, a3, a4, b1, b2, b3, b4;
            float c1, c2, c3, c4, d1, d2, d3, d4;


            float det = res.Det();

            a1 = res.m00;
            b1 = res.m10;
            c1 = res.m20;
            d1 = res.m30;

            a2 = res.m01;
            b2 = res.m11;
            c2 = res.m21;
            d2 = res.m31;

            a3 = res.m02;
            b3 = res.m12;
            c3 = res.m22;
            d3 = res.m32;

            a4 = res.m03;
            b4 = res.m13;
            c4 = res.m23;
            d4 = res.m33;


            res.m00 = res.det3x3(b2, b3, b4, c2, c3, c4, d2, d3, d4) / det;
            res.m01 = -res.det3x3(a2, a3, a4, c2, c3, c4, d2, d3, d4) / det;
            res.m02 = res.det3x3(a2, a3, a4, b2, b3, b4, d2, d3, d4) / det;
            res.m03 = -res.det3x3(a2, a3, a4, b2, b3, b4, c2, c3, c4) / det;

            res.m10 = -res.det3x3(b1, b3, b4, c1, c3, c4, d1, d3, d4) / det;
            res.m11 = res.det3x3(a1, a3, a4, c1, c3, c4, d1, d3, d4) / det;
            res.m12 = -res.det3x3(a1, a3, a4, b1, b3, b4, d1, d3, d4) / det;
            res.m13 = res.det3x3(a1, a3, a4, b1, b3, b4, c1, c3, c4) / det;

            res.m20 = res.det3x3(b1, b2, b4, c1, c2, c4, d1, d2, d4) / det;
            res.m21 = -res.det3x3(a1, a2, a4, c1, c2, c4, d1, d2, d4) / det;
            res.m22 = res.det3x3(a1, a2, a4, b1, b2, b4, d1, d2, d4) / det;
            res.m23 = -res.det3x3(a1, a2, a4, b1, b2, b4, c1, c2, c4) / det;

            res.m30 = -res.det3x3(b1, b2, b3, c1, c2, c3, d1, d2, d3) / det;
            res.m31 = res.det3x3(a1, a2, a3, c1, c2, c3, d1, d2, d3) / det;
            res.m32 = -res.det3x3(a1, a2, a3, b1, b2, b3, d1, d2, d3) / det;
            res.m33 = res.det3x3(a1, a2, a3, b1, b2, b3, c1, c2, c3) / det;

            return res;
        }

        public float Det()
        {
            float a1, a2, a3, a4, b1, b2, b3, b4, c1, c2, c3, c4, d1, d2, d3, d4;

            a1 = this.m00;
            b1 = this.m10;
            c1 = this.m20;
            d1 = this.m30;

            a2 = this.m01;
            b2 = this.m11;
            c2 = this.m21;
            d2 = this.m31;

            a3 = this.m02;
            b3 = this.m12;
            c3 = this.m22;
            d3 = this.m32;

            a4 = this.m03;
            b4 = this.m13;
            c4 = this.m23;
            d4 = this.m33;


            return
                a1 * this.det3x3(b2, b3, b4, c2, c3, c4, d2, d3, d4) -
                b1 * this.det3x3(a2, a3, a4, c2, c3, c4, d2, d3, d4) +
                c1 * this.det3x3(a2, a3, a4, b2, b3, b4, d2, d3, d4) -
                d1 * this.det3x3(a2, a3, a4, b2, b3, b4, c2, c3, c4);
        }


        public Vector TransformVector(Vector v)
        {
            return new Vector(
                v.x * this.m00 + v.y * this.m10 + v.z * this.m20,
                v.x * this.m01 + v.y * this.m11 + v.z * this.m21,
                v.x * this.m02 + v.y * this.m12 + v.z * this.m22);
        }


        public Vector TransformPoint(Vector v)
        {
            return new Vector(
                v.x * this.m00 + v.y * this.m10 + v.z * this.m20 + this.m30,
                v.x * this.m01 + v.y * this.m11 + v.z * this.m21 + this.m31,
                v.x * this.m02 + v.y * this.m12 + v.z * this.m22 + this.m32);
        }


        public Vector TransformPoint(ref Vector aVec)
        {
            float w = m33;

            for (int c = 0; c < 3; c++)
                w += this[3, c] * aVec[c];

            float invW = 1f / w;

            var res = new Vector();

            for (int r = 0; r < 3; r++)
            {
                res[r] = this[r, 3];

                for (int c = 0; c < 3; c++)
                    res[r] += aVec[c] * this[r, c];

                res[r] *= invW;
            }
            return res;
        }

        public static Matrix4x4 operator +(Matrix4x4 m1, Matrix4x4 m2)
        {
            Matrix4x4 r = new Matrix4x4();

            r.m00 = m1.m00 + m2.m00;
            r.m01 = m1.m01 + m2.m01;
            r.m02 = m1.m02 + m2.m02;
            r.m03 = m1.m03 + m2.m03;

            r.m10 = m1.m10 + m2.m10;
            r.m11 = m1.m11 + m2.m11;
            r.m12 = m1.m12 + m2.m12;
            r.m13 = m1.m13 + m2.m13;

            r.m20 = m1.m20 + m2.m20;
            r.m21 = m1.m21 + m2.m21;
            r.m22 = m1.m22 + m2.m22;
            r.m23 = m1.m23 + m2.m23;

            r.m30 = m1.m30 + m2.m30;
            r.m31 = m1.m31 + m2.m31;
            r.m32 = m1.m32 + m2.m32;
            r.m33 = m1.m33 + m2.m33;

            return r;
        }

        public static Matrix4x4 operator -(Matrix4x4 m1, Matrix4x4 m2)
        {
            Matrix4x4 r = new Matrix4x4();

            r.m00 = m1.m00 - m2.m00;
            r.m01 = m1.m01 - m2.m01;
            r.m02 = m1.m02 - m2.m02;
            r.m03 = m1.m03 - m2.m03;

            r.m10 = m1.m10 - m2.m10;
            r.m11 = m1.m11 - m2.m11;
            r.m12 = m1.m12 - m2.m12;
            r.m13 = m1.m13 - m2.m13;

            r.m20 = m1.m20 - m2.m20;
            r.m21 = m1.m21 - m2.m21;
            r.m22 = m1.m22 - m2.m22;
            r.m23 = m1.m23 - m2.m23;

            r.m30 = m1.m30 - m2.m30;
            r.m31 = m1.m31 - m2.m31;
            r.m32 = m1.m32 - m2.m32;
            r.m33 = m1.m33 - m2.m33;

            return r;
        }

        public static Matrix4x4 operator *(Matrix4x4 m1, Matrix4x4 m2)
        {
            Matrix4x4 r = new Matrix4x4();

            r.m00 = m1.m00 * m2.m00 + m1.m01 * m2.m10 + m1.m02 * m2.m20 + m1.m03 * m2.m30;
            r.m01 = m1.m00 * m2.m01 + m1.m01 * m2.m11 + m1.m02 * m2.m21 + m1.m03 * m2.m31;
            r.m02 = m1.m00 * m2.m02 + m1.m01 * m2.m12 + m1.m02 * m2.m22 + m1.m03 * m2.m32;
            r.m03 = m1.m00 * m2.m03 + m1.m01 * m2.m13 + m1.m02 * m2.m23 + m1.m03 * m2.m33;

            r.m10 = m1.m10 * m2.m00 + m1.m11 * m2.m10 + m1.m12 * m2.m20 + m1.m13 * m2.m30;
            r.m11 = m1.m10 * m2.m01 + m1.m11 * m2.m11 + m1.m12 * m2.m21 + m1.m13 * m2.m31;
            r.m12 = m1.m10 * m2.m02 + m1.m11 * m2.m12 + m1.m12 * m2.m22 + m1.m13 * m2.m32;
            r.m13 = m1.m10 * m2.m03 + m1.m11 * m2.m13 + m1.m12 * m2.m23 + m1.m13 * m2.m33;

            r.m20 = m1.m20 * m2.m00 + m1.m21 * m2.m10 + m1.m22 * m2.m20 + m1.m23 * m2.m30;
            r.m21 = m1.m20 * m2.m01 + m1.m21 * m2.m11 + m1.m22 * m2.m21 + m1.m23 * m2.m31;
            r.m22 = m1.m20 * m2.m02 + m1.m21 * m2.m12 + m1.m22 * m2.m22 + m1.m23 * m2.m32;
            r.m23 = m1.m20 * m2.m03 + m1.m21 * m2.m13 + m1.m22 * m2.m23 + m1.m23 * m2.m33;

            r.m30 = m1.m30 * m2.m00 + m1.m31 * m2.m10 + m1.m32 * m2.m20 + m1.m33 * m2.m30;
            r.m31 = m1.m30 * m2.m01 + m1.m31 * m2.m11 + m1.m32 * m2.m21 + m1.m33 * m2.m31;
            r.m32 = m1.m30 * m2.m02 + m1.m31 * m2.m12 + m1.m32 * m2.m22 + m1.m33 * m2.m32;
            r.m33 = m1.m30 * m2.m03 + m1.m31 * m2.m13 + m1.m32 * m2.m23 + m1.m33 * m2.m33;

            return r;
        }



        public byte[] Serialize()
        {
            return new[]
                {
                    BitConverter.GetBytes(m00),
                    BitConverter.GetBytes(m01),
                    BitConverter.GetBytes(m02),
                    BitConverter.GetBytes(m03),
                    BitConverter.GetBytes(m10),
                    BitConverter.GetBytes(m11),
                    BitConverter.GetBytes(m12),
                    BitConverter.GetBytes(m13),
                    BitConverter.GetBytes(m20),
                    BitConverter.GetBytes(m21),
                    BitConverter.GetBytes(m22),
                    BitConverter.GetBytes(m23),
                    BitConverter.GetBytes(m30),
                    BitConverter.GetBytes(m31),
                    BitConverter.GetBytes(m32),
                    BitConverter.GetBytes(m33),
                }.SelectMany(t => t).ToArray();
        }

        public void Deserialize(byte[] data, int offset = 0)
        {
            m00 = BitConverter.ToSingle(data, 0 + offset);
            m01 = BitConverter.ToSingle(data, 4 + offset);
            m02 = BitConverter.ToSingle(data, 8 + offset);
            m03 = BitConverter.ToSingle(data, 12 + offset);

            m10 = BitConverter.ToSingle(data, 16 + offset);
            m11 = BitConverter.ToSingle(data, 20 + offset);
            m12 = BitConverter.ToSingle(data, 24 + offset);
            m13 = BitConverter.ToSingle(data, 28 + offset);

            m20 = BitConverter.ToSingle(data, 32 + offset);
            m21 = BitConverter.ToSingle(data, 36 + offset);
            m22 = BitConverter.ToSingle(data, 40 + offset);
            m23 = BitConverter.ToSingle(data, 44 + offset);

            m30 = BitConverter.ToSingle(data, 48 + offset);
            m31 = BitConverter.ToSingle(data, 52 + offset);
            m32 = BitConverter.ToSingle(data, 56 + offset);
            m33 = BitConverter.ToSingle(data, 60 + offset);
        }
    }
}